﻿//////////////////////////////////////////////////////////////////////////////
//
// Copyright © 1998-2024 Glodon Company Limited.
//
// Licensed under the MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the “Software”),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
//////////////////////////////////////////////////////////////////////////////
#include "GmSizeController.h"
#include <QMouseEvent>
#include <QApplication>
#include <QDesktopWidget>
#include <QCursor>
#include <QWidget>
#include <QtDebug>

#include "EnableCompileWarning_The_LAST_IncludeInCpp.h"

const int kWidth = 6;
static EnHitPosition HitTest(const QRect& rect, const QPoint& pos, int border)
{
    QRect test;
    // LeftTop
    test.setRect(rect.left(), rect.top(), border, border);
    if (test.contains(pos)) {
        return HP_HitTopLeft;
    }

    // RightTop
    test.setRect(rect.right() - border, rect.top(), border, border);
    if (test.contains(pos)) {
        return HP_HitTopRight;
    }

    // LeftBottom
    test.setRect(rect.left(), rect.bottom() - border, border, border);
    if (test.contains(pos)) {
        return HP_HitBottomLeft;
    }

    // RightBottom
    test.setRect(rect.right() - border, rect.bottom() - border, border, border);
    if (test.contains(pos)) {
        return HP_HitBottomRight;
    }

    // LeftFrame
    test.setRect(rect.left(), rect.top() + border, border, rect.height() - 2 * border);
    if (test.contains(pos)) {
        return HP_HitLeft;
    }

    // TopFrame
    test.setRect(rect.left() + border, rect.top(), rect.width() - 2 * border, border);
    if (test.contains(pos)) {
        return HP_HitTop;
    }

    // RightFrame
    test.setRect(test.right(), rect.top() + border, border, rect.height() - 2 * border);
    if (test.contains(pos)) {
        return HP_HitRight;
    }

    // BottomFrame
    test.setRect(rect.left() + border, test.bottom(), rect.width() - 2 * border, border);
    if (test.contains(pos)) {
        return HP_HitBottom;
    }

    return HP_HitNone;
}

GmSizeController::GmSizeController(QWidget* widget, QObject* parent)
    : QObject(parent)
    , m_bEnabled(false)
    , m_pWidget(widget)
    , m_hit(HP_HitNone)
    , m_lastHit(HP_HitNone)
{
}

GmSizeController::~GmSizeController()
{
    m_pWidget->releaseMouse();
}

void GmSizeController::setEnabled(bool enabled)
{
    if (m_bEnabled != enabled) {
        m_bEnabled = (enabled && !isFixedSize(m_pWidget));
        if (enabled) {
            qApp->installEventFilter(this);
        } else {
            qApp->removeEventFilter(this);
        }
    }
}

bool GmSizeController::isFixedSize(QWidget* widget)
{
    Q_ASSERT(widget);
    return ((widget->windowFlags() & Qt::MSWindowsFixedSizeDialogHint)
            || widget->minimumSize() == widget->maximumSize());
}

bool GmSizeController::eventFilter(QObject* obj, QEvent* event)
{
    Q_UNUSED(obj);
    if (!m_bEnabled || m_pWidget->windowState() != Qt::WindowNoState) {
        return false;
    }
    if (event->type() == QEvent::MouseMove) {
        QMouseEvent* me = static_cast<QMouseEvent*>(event);
        if (me->buttons() == Qt::NoButton && me->button() == Qt::NoButton) {
            QWidget* w = QApplication::activeModalWidget();
            if (w == m_pWidget || (!w && m_pWidget->isVisible())) {
                QPoint pos = m_pWidget->mapFromGlobal(me->globalPos());
                QRect rect(QPoint(0, 0), m_pWidget->frameGeometry().size());
                if (rect.contains(pos)) {
                    EnHitPosition hit = HitTest(rect, pos, kWidth);
                    if (hit != HP_HitNone) {
                        m_pWidget->grabMouse();
                    } else {
                        m_pWidget->releaseMouse();
                    }
                    if (m_lastHit != hit) {
                        changeCursor(hit);
                    }
                } else {
                    m_pWidget->releaseMouse();
                    changeCursor(HP_HitNone);
                }
            }
        } else if (me->buttons() == Qt::LeftButton) {
            if (m_hit != HP_HitNone) {
                resizeFrame();
            }
        }
    } else if (event->type() == QEvent::MouseButtonRelease) {
        QMouseEvent* me = static_cast<QMouseEvent*>(event);
        mouseReleaseEvent(me);
    } else if (event->type() == QEvent::MouseButtonPress) {
        QMouseEvent* me = static_cast<QMouseEvent*>(event);
        mousePressEvent(me);
    } else if (obj == m_pWidget &&
               (event->type() == QEvent::Hide || event->type() == QEvent::Close)) {
        m_pWidget->releaseMouse();
    }
    return false;
}

void GmSizeController::mousePressEvent(QMouseEvent* event)
{
    if (event->button() == Qt::LeftButton) {
        QWidget* w = QApplication::activeModalWidget();
        if (w == m_pWidget || (!w && m_pWidget->isVisible())) {
            QRect rect(QPoint(0, 0), m_pWidget->frameGeometry().size());
            const QPoint& global_pos = event->globalPos();
            EnHitPosition hit = HitTest(rect, m_pWidget->mapFromGlobal(global_pos), kWidth);
            if (hit != HP_HitNone) {
                m_hit = hit;
                m_startPosition = global_pos;
                if (QWidget* w = QApplication::focusWidget()) {
                    w->clearFocus();
                }
                m_pWidget->grabMouse();
            }
        }
    }
}

void GmSizeController::mouseReleaseEvent(QMouseEvent* event)
{
    if (m_hit != HP_HitNone) {
        m_hit = HP_HitNone;
        m_startPosition = QPoint();
    }
    QRect rect(QPoint(0, 0), m_pWidget->frameGeometry().size());
    EnHitPosition hit = HitTest(rect, m_pWidget->mapFromGlobal(event->globalPos()), kWidth);
    if (m_lastHit != hit) {
        if (hit == HP_HitNone) {
            m_pWidget->releaseMouse();
        }
        changeCursor(hit);
    }
}

void GmSizeController::resizeFrame()
{
    QRect clip_rect = QApplication::desktop()->geometry();
    QPoint cursorPos = QCursor::pos();
    if (cursorPos.x() < clip_rect.left()) {
        cursorPos.setX(clip_rect.left());
    } else if (cursorPos.x() > clip_rect.right()) {
        cursorPos.setX(clip_rect.right());
    }
    if (cursorPos.y() < clip_rect.top()) {
        cursorPos.setY(clip_rect.top());
    } else if (cursorPos.y() > clip_rect.bottom()) {
        cursorPos.setY(clip_rect.bottom());
    }

    QPoint offset = cursorPos - m_startPosition;
    bool to_left = (offset.x() < 0);
    bool to_down = (offset.y() > 0);
    QRect new_rect = m_pWidget->frameGeometry();
    QPoint left_line(0, 0);
    QPoint top_line(0, 0);
    QPoint right_line(new_rect.width(), 0);
    QPoint bottom_line(0, new_rect.height());

    switch (m_hit) {
    case HP_HitLeft:
        if (to_left) {
            left_line = m_pWidget->mapToGlobal(left_line);
            if (left_line.x() < cursorPos.x()) {
                m_startPosition = cursorPos;
                return;
            }
        }
        new_rect.setLeft(new_rect.left() + offset.x());
        break;
    case HP_HitTop:
        if (!to_down) {
            top_line = m_pWidget->mapToGlobal(top_line);
            if (top_line.y() < cursorPos.y()) {
                m_startPosition = cursorPos;
                return;
            }
        }
        new_rect.setTop(new_rect.top() + offset.y());
        break;
    case HP_HitRight:
        if (!to_left) {
            right_line = m_pWidget->mapToGlobal(right_line);
            if (right_line.x() > cursorPos.x()) {
                m_startPosition = cursorPos;
                return;
            }
        }
        new_rect.setRight(new_rect.right() + offset.x());
        break;
    case HP_HitBottom:
        if (to_down) {
            bottom_line = m_pWidget->mapToGlobal(bottom_line);
            if (bottom_line.y() > cursorPos.y()) {
                m_startPosition = cursorPos;
                return;
            }
        }
        new_rect.setBottom(new_rect.bottom() + offset.y());
        break;
    case HP_HitTopLeft:
        if (to_left) {
            left_line = m_pWidget->mapToGlobal(left_line);
            if (left_line.x() < cursorPos.x()) {
                offset.setX(0);
            }
        }
        if (!to_down) {
            top_line = m_pWidget->mapToGlobal(top_line);
            if (top_line.y() < cursorPos.y()) {
                offset.setY(0);
            }
        }
        if (offset.x() == 0 && offset.y() == 0) {
            m_startPosition = cursorPos;
            return;
        }
        new_rect.setLeft(new_rect.left() + offset.x());
        new_rect.setTop(new_rect.top() + offset.y());
        break;
    case HP_HitTopRight:
        if (!to_left) {
            right_line = m_pWidget->mapToGlobal(right_line);
            if (right_line.x() > cursorPos.x()) {
                offset.setX(0);
            }
        }
        if (!to_down) {
            top_line = m_pWidget->mapToGlobal(top_line);
            if (top_line.y() < cursorPos.y()) {
                offset.setY(0);
            }
        }
        if (offset.x() == 0 && offset.y() == 0) {
            m_startPosition = cursorPos;
            return;
        }
        new_rect.setRight(new_rect.right() + offset.x());
        new_rect.setTop(new_rect.top() + offset.y());
        break;
    case HP_HitBottomLeft:
        if (to_left) {
            left_line = m_pWidget->mapToGlobal(left_line);
            if (left_line.x() < cursorPos.x()) {
                offset.setX(0);
            }
        }
        if (to_down) {
            bottom_line = m_pWidget->mapToGlobal(bottom_line);
            if (bottom_line.y() > cursorPos.y()) {
                offset.setY(0);
            }
        }
        if (offset.x() == 0 && offset.y() == 0) {
            m_startPosition = cursorPos;
            return;
        }
        new_rect.setLeft(new_rect.left() + offset.x());
        new_rect.setBottom(new_rect.bottom() + offset.y());
        break;
    case HP_HitBottomRight:
        if (!to_left) {
            right_line = m_pWidget->mapToGlobal(right_line);
            if (right_line.x() > cursorPos.x()) {
                offset.setX(0);
            }
        }
        if (to_down) {
            bottom_line = m_pWidget->mapToGlobal(bottom_line);
            if (bottom_line.y() > cursorPos.y()) {
                offset.setY(0);
            }
        }
        if (offset.x() == 0 && offset.y() == 0) {
            m_startPosition = cursorPos;
            return;
        }
        new_rect.setRight(new_rect.right() + offset.x());
        new_rect.setBottom(new_rect.bottom() + offset.y());
        break;
    }
    int left = new_rect.left();
    int top = new_rect.top();
    int right = new_rect.right();
    int bottom = new_rect.bottom();
    if (new_rect.width() < m_pWidget->minimumWidth()) {
        left = m_pWidget->frameGeometry().left();
        right = m_pWidget->frameGeometry().right();
    }
    if (new_rect.height() < m_pWidget->minimumHeight()) {
        top = m_pWidget->frameGeometry().top();
        bottom = m_pWidget->frameGeometry().bottom();
    }
    new_rect.setCoords(left, top, right, bottom);
    m_pWidget->setGeometry(new_rect);
    m_startPosition = cursorPos;
}

void GmSizeController::changeCursor(EnHitPosition hit)
{
    if (m_lastHit != HP_HitNone) {
        m_pWidget->setCursor(QCursor(Qt::ArrowCursor));
    }

    switch (hit) {
    case HP_HitLeft:
    case HP_HitRight:
        m_pWidget->setCursor(QCursor(Qt::SizeHorCursor));
        break;
    case HP_HitTop:
    case HP_HitBottom:
        m_pWidget->setCursor(QCursor(Qt::SizeVerCursor));
        break;
    case HP_HitTopLeft:
    case HP_HitBottomRight:
        m_pWidget->setCursor(QCursor(Qt::SizeFDiagCursor));
        break;
    case HP_HitTopRight:
    case HP_HitBottomLeft:
        m_pWidget->setCursor(QCursor(Qt::SizeBDiagCursor));
        break;
    default:
        break;
    }

    m_lastHit = hit;
}
